package server.proxy;


import java.io.IOException;
import java.net.*;
import java.nio.charset.Charset;
import java.util.concurrent.Executor;

import logger.CLoggerFactory;

import org.apache.log4j.Logger;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.handler.codec.http.DefaultHttpRequest;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpRequestEncoder;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseDecoder;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;

/**
 * This class is handling the incoming requests to the server. 
 * 
 * @author Maxim
 *
 */
public class ProxyServerHandler extends SimpleChannelUpstreamHandler {
	
	private volatile Channel _proxyServerChanel;
	private Executor _threadPool;
	private String _remoteHost;
	private int _remotePort;
	private RemoteHostHandler _remoteHostHandler;
	private HttpHost _httpHost;
	private Logger _logger;
	
	
	public ProxyServerHandler(Executor threadPool) throws IOException{
		
		this._threadPool = threadPool;
		this._logger = CLoggerFactory.getFileLogger("requestlog", Config.REQUEST_LOG_FILE_NAME);
	}
	
	
    public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) {
        // Add all open channels to the global group so that they are
        // closed on shutdown.
       
    	ProxyServer.allChannels.add(e.getChannel());
    }
	
	
	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception{
		
		HttpRequest tHttpRequest = (HttpRequest)e.getMessage();
	
		String message = "Request from: "+parseSenderIP(e.getChannel().getRemoteAddress().toString())+"\n" +tHttpRequest.toString();
		this._logger.info(message);
		
		this._proxyServerChanel = e.getChannel();

		
		
		switch(Config.SERVER_RUNNING_MODE){
			
			case 1: 
				tHttpRequest = openMode(tHttpRequest, ctx);
				break;
			case 2:
				anonymousMode(tHttpRequest);
				break;
		}
		
		
		
		//In case the HTTP request in older version we will convert it to the new version.
		
		if(tHttpRequest.getProtocolVersion() == HttpVersion.HTTP_1_0)
			
			tHttpRequest = convertRequestTo1_1(tHttpRequest);
		
		
		//Create the pipe for the connection to the remote host. 
		
		ClientBootstrap tBootstrap = new ClientBootstrap(new NioClientSocketChannelFactory(this._threadPool, this._threadPool));
		ChannelPipeline tChannelPipeline = tBootstrap.getPipeline();
		tChannelPipeline.addLast("decoder", new HttpResponseDecoder());
		tChannelPipeline.addLast("aggregator", new HttpChunkAggregator(1048576));
		tChannelPipeline.addLast("encoder", new HttpRequestEncoder());
		
		tChannelPipeline.addLast("recievedMailListHandler", new ProxyServerMailListHandler());
		
		this._remoteHostHandler = new RemoteHostHandler(tHttpRequest);
		tChannelPipeline.addLast("handler", this._remoteHostHandler);
		
		
		
		
		//extracting remote host address & establishing connection to the remote host
		
		//default port 
		this._remotePort = 80;
		
		String[] tRemoteHostAddress = tHttpRequest.getHeader(HttpHeaders.Names.HOST).split(":");
		this._remoteHost = tRemoteHostAddress[0];
		
		if(tRemoteHostAddress.length == 2){
			
			this._remotePort = Integer.parseInt(tRemoteHostAddress[1]);
			
		}
		
		_httpHost = new HttpHost(this._remoteHost, this._remotePort);
		
		InetSocketAddress tDestination = new InetSocketAddress(this._httpHost.getHost(), this._httpHost.getPort());
		
		ChannelFuture tConnectFuture = tBootstrap.connect(tDestination);
		
		tConnectFuture.addListener(new ChannelFutureListener() {
        	
			public void operationComplete(ChannelFuture future) throws Exception {
				if (future.isSuccess()) {
        	                  
					future.getChannel().getCloseFuture().addListener(new ChannelFutureListener() {
        	
						public void operationComplete(ChannelFuture future) throws Exception {
							writeResponseAndClose(_remoteHostHandler.getResponse());
        	                        }
        	                   });
                } 
				
				else {
					future.getChannel().close();
                    writeResponseAndClose(createErrorResponse("Could not connect to " + _remoteHost + ":" + _remotePort));
				}
        	}
        	});
		
		
	}
	
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
		
		System.out.println("Unexpected exception from proxy server handler: " + e.getCause());
		e.getChannel().close();
	}
	
	private void writeResponseAndClose(HttpResponse response) {
		if (response != null) {
           response.setHeader("Connection", "close");
           this._proxyServerChanel.write(response).addListener(ChannelFutureListener.CLOSE);
		} 
		
		else {
	        this._proxyServerChanel.close();
	    }
	}
	
	/**
	 * This method convert HTTP 1.0 to HTTP 1.1
	 * @param request
	 * @return
	 * @throws URISyntaxException
	 */
	
	private HttpRequest convertRequestTo1_1(HttpRequest request) throws URISyntaxException {
	    	
		DefaultHttpRequest tNewReq = new DefaultHttpRequest(HttpVersion.HTTP_1_1, request.getMethod(), request.getUri());
	    if (!request.getHeaderNames().isEmpty()) {
	    
	    for (String name : request.getHeaderNames()) {
	    	tNewReq.setHeader(name, request.getHeaders(name));
	            }
	        }
	    if (!tNewReq.containsHeader(HttpHeaders.Names.HOST)) {
	        
	    	URI url = new URI(tNewReq.getUri());
	        String host = url.getHost();
	        if (url.getPort() != -1) {
	            host += ":" + url.getPort();
	        }
	        tNewReq.setHeader(HttpHeaders.Names.HOST, host);
	        
	     }
	        tNewReq.setContent(request.getContent());
	        return tNewReq;
	 }
	
 	 @SuppressWarnings("deprecation")
	 public static HttpResponse createErrorResponse(String errorText) {
		 
		 HttpResponse tResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
		 tResponse.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/html; charset=utf-8");
	     
		 ChannelBuffer tBuf = ChannelBuffers.copiedBuffer("<html><body><h3>" + errorText + "</h3></body></html>", "utf-8");
	     tResponse.setContent(tBuf);
	     tResponse.setHeader("Content-Length", String.valueOf(tBuf.readableBytes()));
	     return tResponse;
    }
 	 
 	 
 	 
 	 /**
 	  * This method change the HTTP request according to open mode policy
 	  * @param HttpRequest
 	  * @param ctx
 	  * @return
 	  */
 	 private HttpRequest openMode(HttpRequest HttpRequest, ChannelHandlerContext ctx){
 		
 		 HttpRequest tHttpRequest =  HttpRequest;
 		
 		 //Keep alive option not supported yet
 		 tHttpRequest.removeHeader("Proxy-Connection");
 		 tHttpRequest.setHeader("Connection", "close");
 		 
 		 
 		 
 		 tHttpRequest.setHeader("x-forwarded-for", ctx.getChannel().getRemoteAddress());
 		 tHttpRequest.setHeader("proxy-version", Config.PROXY_SERVER_VERSION);
 		 
 		 
 		 return tHttpRequest;
 	 }
 	 
 	 
 	 /**
 	  * This method change the HTTP request according to anonymous mode policy
 	  * 
 	  * @param HttpRequest
 	  * @return
 	  */
 	 private HttpRequest anonymousMode(HttpRequest HttpRequest){
 		 
 		 HttpRequest tHttpRequest =  HttpRequest;
 		 
 		//Keep alive option not supported yet
 		 tHttpRequest.removeHeader("Proxy-Connection");
 		 tHttpRequest.setHeader("Connection", "close");
 		 
 		 tHttpRequest.removeHeader("Cookie");
 		 tHttpRequest.setHeader("User-Agent", "Generic User-agent");
		 
 		 
 		 return tHttpRequest;
 		 
 	 }
 	 
 	/**
 	 * 
 	 * This method extracts the sender IP from remote address
 	 * @param senderIP remote address
 	 * @return String representation in form *.*.*.*
 	 * 
 	 */
 	private String parseSenderIP(String senderIP){
 		
 		String tAnswer = ""; 
 		String tDigits = "0123456789";
 		byte [] tSenderIP = senderIP.getBytes(Charset.defaultCharset());
 		int tSenderIPLength = tSenderIP.length;
 		
 		for(int j = 0; j < tSenderIPLength; j++){
 			if((char)tSenderIP[j]== ':')
 				break;
 			
 			if((char)tSenderIP[j]== '.'  | tDigits.indexOf((char)tSenderIP[j]) != -1 )
 				tAnswer = tAnswer.concat(String.valueOf((char)tSenderIP[j]));
 		}
 		return tAnswer;
 	}
 	 
 	 

}
